package md.sv.dbu.executor;

import md.sv.dbu.DBU;
import md.sv.dbu.DBUExecutor;
import md.sv.dbu.config.jaxb.*;
import md.sv.dbu.db.DBExecutor;
import md.sv.dbu.utils.DbuUtils;
import org.apache.commons.io.FilenameUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.w3c.dom.Document;
import org.w3c.dom.Node;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
 * @author MG
 */
public class SendTaskExecutor implements DbuTypeExecutor {
    private static final Logger logger = LoggerFactory.getLogger(SendTaskExecutor.class);

    private DBUExecutor dbuExecutor;

    @Override
    public void registerMainExecutor(DBUExecutor executor) {
        this.dbuExecutor = executor;
    }

    @Override
    public boolean isEligible(Object dbuElement) {
        return dbuElement instanceof SendTaskType;
    }

    @Override
    public void execute(Object dbuElement) {
        SendTaskType sendTask = (SendTaskType) dbuElement;
        MqConfigType mqConfig = null;

        if (!sendTask.isExecute()) {
            return;
        }

        mqConfig = sendTask.getMqConfig();
        if (!dbuExecutor.getMessageSender().connect(mqConfig.getManager(), mqConfig.getQueueTo(), mqConfig.isByteMessage())) {
            logger.error("Could not connect to Queue [{}] for the MQ Manager [{}]", mqConfig.getManager(), mqConfig.getQueueTo());
            return;
        }

        String configPath = FilenameUtils.concat(dbuExecutor.getRootPath(), sendTask.getConfigFile());
        if (!DbuUtils.fileExists(configPath)) {
            logger.error("Config File {} does not exist or is not readable.", configPath);
            return;
        }
        DbuType configDbu;
        try {
            configDbu = dbuExecutor.getXmlParser().parseConfigFile(DbuUtils.openFile(configPath), DbuType.class, DBU.class.getResourceAsStream("configXMD.xsd"));
        } catch (Exception e) {
            logger.error("Could not parse config file {}", configPath);
            logger.error("Underlying error: ", e);
            return;
        }

        if (configDbu.getConfigurationOrUpdateTaskOrReceiveMQ().size() == 0 ||
                !(configDbu.getConfigurationOrUpdateTaskOrReceiveMQ().get(0) instanceof SqlConfigType)) {
            logger.error("Config File {} is not an SQL Config", configPath);
            return;
        }
        executeSendTask(sendTask, (SqlConfigType) configDbu.getConfigurationOrUpdateTaskOrReceiveMQ().get(0));
        dbuExecutor.getMessageSender().close();

    }

    private void executeSendTask(SendTaskType sendTask, SqlConfigType sqlConfig) {
        if (!sendTask.isExecute()) {
            return;
        }
        logger.info("Executing send task: {}", sendTask.getDescription());

        dbuExecutor.getVariables().put(sendTask.getNewIdVariableName(), dbuExecutor.generateNewGUID());

        boolean hasError = false;

        String templatePath = FilenameUtils.concat(dbuExecutor.getRootPath(), sendTask.getTemplateFile());
        if (!DbuUtils.fileExists(templatePath)) {
            logger.error("Template File {} does not exist or is not readable.", templatePath);
            return;
        }

        if (sqlConfig.getSelectSQL().size() == 0) {
            logger.error("Select SQL should be provided in template for the send task: {}", sendTask.getDescription());
            return;
        }

        DBExecutor executor = dbuExecutor.getDbExecutors().get(sendTask.getDatabase());
        executor = dbuExecutor.getExecutor(executor, sqlConfig.getSelectSQL().get(0).getDatabase());
        if (executor == null) {
            logger.error("Database connection is not provided or is incorrect for the send task: {}", sendTask.getDescription());
            return;
        }

        List<MapSqlParameterSource> valuesList = executor.selectQuery(sqlConfig.getSelectSQL().get(0).getValue(), new MapSqlParameterSource(dbuExecutor.getVariables()), dbuExecutor.getVariables());

        String xmlTemplate = DbuUtils.readFile(templatePath);
        if (xmlTemplate == null) {
            logger.error("Could not read template file: {}", templatePath);
            return;
        }

        dbuExecutor.getVariables().put(sendTask.getNewIdVariableName(), UUID.randomUUID().toString());

        for (MapSqlParameterSource parameterSource : valuesList) {
            String xmlFile = null;
            if (sqlConfig.getSelectSQL().size() > 1) {
                DBExecutor selectExecutor = dbuExecutor.getExecutor(executor, sqlConfig.getSelectSQL().get(1).getDatabase());
                if (selectExecutor == null) {
                    logger.warn("DB executor {} defined for select query is invalid in send task: {}", sqlConfig.getSelectSQL().get(1).getDatabase(), sendTask.getDescription());
                    continue;
                }
                List<MapSqlParameterSource> rowValues = selectExecutor.selectQuery(sqlConfig.getSelectSQL().get(1).getValue(), parameterSource, dbuExecutor.getVariables());
                if (!rowValues.isEmpty()) {
                    List<String> simpleXmlDocuments = new ArrayList<String>();
                    for (MapSqlParameterSource row : rowValues) {
                        MapSqlParameterSource currentParamSource = new MapSqlParameterSource(parameterSource.getValues());
                        currentParamSource.addValues(row.getValues());
                        simpleXmlDocuments.add(getSimpleXmlDocFromTemplate(xmlTemplate, currentParamSource));
                    }
                    xmlFile = unifyDocuments(simpleXmlDocuments);
                }
            }

            if (xmlFile == null) {
                logger.warn("XML File for send task [{}] is null. Please verify that 2 SQLs are provided: One base sql, and one SQL for data population; and that the population SQL returns at least one row", sendTask.getDescription());
                continue;
            }

            if (dbuExecutor.getSqlVariablesReplacer().hasVariables(xmlFile) || dbuExecutor.getGlobalVariablesReplacer().hasVariables(xmlFile)) {
                hasError = true;
            }

            if (!hasError && sendTask.isValidate()) {
                String validatePath = FilenameUtils.concat(dbuExecutor.getRootPath(), sendTask.getValidateFile());
                if (DbuUtils.fileExists(validatePath)) {
                    String validateFile = DbuUtils.readFile(validatePath);
                    if (!dbuExecutor.getXmlParser().validateXml(xmlFile, validateFile)) {
                        logger.error("XML file validation failed for Send task: {}", sendTask.getDescription());
                        hasError = true;
                    }
                } else {
                    logger.error("Could not validate. Validation File {} does not exist or is not readable.", templatePath);
                    hasError = true;
                }
            }

            if (sendTask.isSendOnError() || !hasError) {
                logger.debug("Sending generated xml to MQ");
                dbuExecutor.getMessageSender().sendMessage(xmlFile);
            }

            if (sendTask.getFileLogger().isEnabled()) {
                StringBuilder logFileName = new StringBuilder(sendTask.getFileLogger().getPatternFileName());
                dbuExecutor.getSqlVariablesReplacer().replaceVariables(logFileName, parameterSource.getValues(), dbuExecutor.getDateTimeFormats());
                dbuExecutor.getGlobalVariablesReplacer().replaceVariables(logFileName, dbuExecutor.getVariables(), dbuExecutor.getDateTimeFormats());
                String logFilePath = FilenameUtils.concat(FilenameUtils.concat(dbuExecutor.getRootPath(), sendTask.getFileLogger().getPath()), logFileName.toString());
                logger.debug("Saving XML file to {}", logFilePath);
                DbuUtils.writeFile(logFilePath, xmlFile);

                String variableValue = FilenameUtils.concat(sendTask.getFileLogger().getPath(), logFileName.toString());
                parameterSource.addValue(sendTask.getFileLogger().getVariableName(), variableValue);
            }

            if (sendTask.getDbLogger().isLogOnError() || !hasError) {
                for (UpdateBlockType updateBlock : sqlConfig.getUpdateBlock()) {
                    DBExecutor.executeUpdateBlock(dbuExecutor, updateBlock, executor, parameterSource);
                }

                for (SwitchType switchType : sqlConfig.getSwitch()) {
                    Object value = parameterSource.getValues().get(switchType.getVariable());
                    if (value == null) {
                        logger.warn("Variable {} specified for switch, but it has no values", switchType.getVariable());
                        continue;
                    }
                    String checkValue = value.toString();
                    boolean caseExecuted = false;
                    for (CaseType caseType : switchType.getCase()) {
                        if (caseType.getValue().equals(checkValue)) {
                            for (UpdateBlockType updateBlock : caseType.getUpdateBlock()) {
                                DBExecutor.executeUpdateBlock(dbuExecutor, updateBlock, executor, parameterSource);
                            }
                            caseExecuted = true;
                            break;
                        }
                    }
                    if (!caseExecuted) {
                        for (UpdateBlockType updateBlock : switchType.getDefault().getUpdateBlock()) {
                            DBExecutor.executeUpdateBlock(dbuExecutor, updateBlock, executor, parameterSource);
                        }
                    }
                }
            }
        }

    }

    private String getSimpleXmlDocFromTemplate(String xmlTemplate, MapSqlParameterSource parameterSource) {
        StringBuilder xml = new StringBuilder(xmlTemplate);
        if (dbuExecutor.getSqlVariablesReplacer().replaceVariables(xml, parameterSource.getValues(), dbuExecutor.getDateTimeFormats()) > 0) {
            logger.debug("Not all SQL variables could be replaced");
        }
        if (dbuExecutor.getGlobalVariablesReplacer().replaceVariables(xml, dbuExecutor.getVariables(), dbuExecutor.getDateTimeFormats()) > 0) {
            logger.debug("Not all Global variables could be replaced");
        }
        return xml.toString();
    }

    private String unifyDocuments(List<String> simpleXmlDocuments) {
        if (simpleXmlDocuments.isEmpty()) {
            return null;
        }

        Document baseXml = null;
        for (String simpleXml : simpleXmlDocuments) {
            if (baseXml == null) {
                // set the first simple document as base
                baseXml = DbuUtils.getDocumentFromString(simpleXml);
            } else {
                // add the rest simple documents to the base one (only the differentiating tags)
                Document simpleDoc = DbuUtils.getDocumentFromString(simpleXml);
                compareNodes(simpleDoc.getFirstChild(), baseXml.getFirstChild(), baseXml);
            }
        }

        return DbuUtils.toString(baseXml);
    }


    private boolean compareNodes(Node simpleNode, Node baseNode, Document baseDoc) {
        if (simpleNode.getFirstChild() != null && simpleNode.getFirstChild().getNodeType() != Node.TEXT_NODE
                || simpleNode.getChildNodes().getLength() > 1) { // parent node
            boolean equal = true;
            Node baseParentNode = baseNode.getParentNode();
            for (int k = 0; k < baseParentNode.getChildNodes().getLength(); k++) {
                Node iBaseNode = baseParentNode.getChildNodes().item(k);
                if (!simpleNode.getNodeName().equals(iBaseNode.getNodeName())) {
                    continue;
                }
                equal = true;
                for (int i = 0; i < simpleNode.getChildNodes().getLength(); i++) {
                    Node simpleChild = simpleNode.getChildNodes().item(i);
                    if (simpleChild.getNodeType() != Node.ELEMENT_NODE) {
                        continue;
                    }

                    for (int j = 0; j < iBaseNode.getChildNodes().getLength(); j++) {
                        Node baseChild = iBaseNode.getChildNodes().item(j);
                        if (baseChild.getNodeType() != Node.ELEMENT_NODE) {
                            continue;
                        }
                        if (simpleChild.getNodeName().equals(baseChild.getNodeName())) {
                            if (!compareNodes(simpleChild, baseChild, baseDoc)) {
                                equal = false;
                            }
                            break;
                        }
                    }
                    if (!equal) {
                        break;
                    }
                }
                if (equal) {
                    break;
                }
            }
            if (!equal) {
                Node importedNode = baseDoc.importNode(simpleNode, true);
                baseNode.getParentNode().insertBefore(importedNode, baseNode.getNextSibling());
            }
            return true;
        } else {
             return simpleNode.getTextContent().equals(baseNode.getTextContent());
        }
    }
}
